package com.android.systemui.ios;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.Surface;
import android.view.WindowManager;

/**
 * @hide
 */
public final class ScreenBlurTask {

    /**
     * @see #METHOD_JAVA
     * @see #METHOD_RENDERSCRIPT
     * @see #setMethod(int)
     */
    public static final int METHOD_NONE = -1;

    /**
     * @see #METHOD_NONE
     * @see #METHOD_RENDERSCRIPT
     * @see #setMethod(int)
     */
    public static final int METHOD_JAVA = 0;

    /**
     * @see #METHOD_NONE
     * @see #METHOD_JAVA
     * @see #setMethod(int)
     */
    public static final int METHOD_RENDERSCRIPT = 1;

    private final Context mContext;
    private final Rect mScreenRect = new Rect();
    private final Rect mCropRect = new Rect();

    private int mRadius = 95;
    private int mScale = -1;
    private int mAlpha = 255;
    private int mOverlayColor = 0;
    private int mBlurMethod = METHOD_JAVA;
    private Display mDisplay;
    private Bitmap mSourceBitmap = null;

    private Callback mCallback = null;

    private boolean hasStarted = false;

    /**
     * Create.
     *
     * @param context
     * @return
     */
    public static ScreenBlurTask from(Context context) {
        return from(context, null);
    }

    /**
     * Create.
     *
     * @param context
     * @param src
     * @return
     */
    public static ScreenBlurTask from(Context context, Bitmap src) {
        return new ScreenBlurTask(context, src);
    }

    /**
     * Construction
     *
     * @param context
     * @param bitmap
     */
    private ScreenBlurTask(Context context, Bitmap bitmap) {
        mContext = context;

        hasStarted = false;
        mSourceBitmap = bitmap;
        if (mSourceBitmap != null) {
            mScreenRect.set(0, 0, mSourceBitmap.getWidth(), mSourceBitmap.getHeight());
        } else {
            DisplayMetrics metrics = new DisplayMetrics();
            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
            mDisplay = wm.getDefaultDisplay();
            mDisplay.getRealMetrics(metrics);
            mScreenRect.set(0, 0, metrics.widthPixels, metrics.heightPixels);
        }
        mCropRect.set(mScreenRect);
    }

    public ScreenBlurTask setBitmap(Bitmap bitmap) {
        mSourceBitmap = bitmap;
        return this;
    }

    /**
     * Set alpha 0~255.
     *
     * @param alpha
     * @return
     */
    public final ScreenBlurTask setAlpha(int alpha) {
        mAlpha = Math.max(Math.min(alpha, 255), 0);
        return this;
    }

    /**
     * Set up the fuzzy radius. If 0, is not for gaussian blur.
     *
     * @param radius
     * @return
     */
    public final ScreenBlurTask setRadius(int radius) {
        mRadius = Math.max(Math.min(radius, 100), 0);
        return this;
    }

    /**
     * Scale down.
     *
     * @param scale
     * @return
     */
    public final ScreenBlurTask setScaleDown(int scale) {
        mScale = Math.max(scale, 0);
        return this;
    }

    /**
     * Blur method.
     *
     * @param method
     * @return
     */
    public final ScreenBlurTask setMethod(int method) {
        mBlurMethod = method;
        return this;
    }

    /**
     * Cut the designated area.
     *
     * @param x
     * @param y
     * @param width
     * @param height
     * @return
     */
    public final ScreenBlurTask setClipRegion(int x, int y, int width, int height) {
        if (!mCropRect.intersect(x, y, x + width, y + height)) {
            throw new NullPointerException("ScreenBlurTask.setClipRegion(). Cutting area is invalid. ");
        }
        return this;
    }

    /**
     * Cut the designated area.
     *
     * @param rect
     * @return
     */
    public final ScreenBlurTask setClipRegion(Rect rect) {
        if (!mCropRect.intersect(rect)) {
            throw new NullPointerException("ScreenBlurTask.setClipRegion(). Cutting area is invalid. ");
        }
        return this;
    }

    /**
     * Set the color overlay.
     *
     * @param color
     * @return
     */
    public final ScreenBlurTask setOverlayColor(int color) {
        mOverlayColor = color;
        return this;
    }

    /**
     * After executing the method, can be blurred.
     */
    public final void generate(Callback callback) {
        if (hasStarted) {
            return;
        }
        mCallback = callback;
        if (mCallback == null) {
            return;
        }
        new ShotTask().execute();
    }

    /**
     * Synchronous processing, More time-consuming.
     *
     * @return
     */
    public final Bitmap generate() {
        Bitmap resBitmap = null;
        if (mSourceBitmap != null) {
            resBitmap = mSourceBitmap;
        } else {
            final int width = mScreenRect.width();
            final int height = mScreenRect.height();
            final Matrix displayMatrix = new Matrix();
            final float degrees = getDegreesForRotation(mDisplay.getRotation());
            final boolean requiresRotation = (degrees > 0);
            float[] dims = {width, height};
            if (requiresRotation) {
                displayMatrix.reset();
                displayMatrix.preRotate(-degrees);
                displayMatrix.mapPoints(dims);
            }
            resBitmap = null;//ExtDeviceManager.screenshot(width, height);
            if (resBitmap == null) {
                return null;
            }
            if (requiresRotation) {
                Bitmap ss = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                Canvas c = new Canvas(ss);
                c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
                c.rotate(degrees);
                c.translate(-dims[0] / 2, -dims[1] / 2);
                c.drawBitmap(resBitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
                c.setBitmap(null);
                resBitmap = ss;
            }
        }
        if (resBitmap != null) {
            resBitmap = Bitmap.createBitmap(resBitmap, mCropRect.left, mCropRect.top, mCropRect.width(), mCropRect.height());
        }
        if (!resBitmap.isMutable()) {
            resBitmap = resBitmap.copy(resBitmap.getConfig(), true);
        }

        if (mRadius > 0) {
            int scale = mRadius / 25 + 1;
            int radius = mRadius * 25 / 100;
            if (mBlurMethod == METHOD_JAVA) {
                scale = 6;
                radius = mRadius / 8;
            }
            if (mScale >= 0) {
                scale = mScale;
            }
            if (scale >= 1) {
                int scaleWidth = mCropRect.width() / Math.max(1, scale + 1);
                int scaleHeight = mCropRect.height() / Math.max(1, scale + 1);
                if (scaleWidth <= 0) {
                    scaleWidth = mCropRect.width();
                }
                if (scaleHeight <= 0) {
                    scaleHeight = mCropRect.height();
                }
                resBitmap = Bitmap.createScaledBitmap(resBitmap, scaleWidth, scaleHeight, true);
            }
            switch (mBlurMethod) {
                case METHOD_NONE:
                    // None.
                    break;
                case METHOD_RENDERSCRIPT:
                    resBitmap = blurInRenderScript(resBitmap, radius);
                    break;
                case METHOD_JAVA:
                default:
                    resBitmap = blurInJava(resBitmap, radius);
                    break;
            }
            resBitmap = Bitmap.createScaledBitmap(resBitmap, mCropRect.width(), mCropRect.height(), true);
        }
        if (Color.alpha(mOverlayColor) > 0) {
            int bw = resBitmap.getWidth();
            int bh = resBitmap.getHeight();
            Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
            Bitmap ss = Bitmap.createBitmap(bw, bh, Bitmap.Config.ARGB_8888);
            Canvas c = new Canvas(ss);
            c.drawBitmap(resBitmap, 0, 0, paint);
            c.drawColor(mOverlayColor);
            c.setBitmap(null);
            resBitmap = ss;
        }
        return getAlphaBitmap(resBitmap);
    }

    private Bitmap getAlphaBitmap(Bitmap sourceImg) {
        if (mAlpha < 255) {
            int[] argb = new int[sourceImg.getWidth() * sourceImg.getHeight()];
            sourceImg.getPixels(argb, 0, sourceImg.getWidth(), 0, 0, sourceImg.getWidth(), sourceImg.getHeight());
            for (int i = 0; i < argb.length; i++) {
                argb[i] = (mAlpha << 24) | (argb[i] & 0x00FFFFFF);
            }
            sourceImg = Bitmap.createBitmap(argb, sourceImg.getWidth(), sourceImg.getHeight(), Bitmap.Config.ARGB_8888);
        }
        return sourceImg;
    }

    private float getDegreesForRotation(int value) {
        switch (value) {
            case Surface.ROTATION_90:
                return 360f - 90f;
            case Surface.ROTATION_180:
                return 360f - 180f;
            case Surface.ROTATION_270:
                return 360f - 270f;
        }
        return 0f;
    }

    /**
     * @param bitmap
     * @param radius
     * @return
     * @see #blurInRenderScript(Bitmap, int)
     */
    private Bitmap blurInJava(Bitmap bitmap, int radius) {
        if (radius <= 1) {
            return bitmap;
        }

        int w = bitmap.getWidth();
        int h = bitmap.getHeight();
        int[] pix = new int[w * h];
        bitmap.getPixels(pix, 0, w, 0, 0, w, h);

        int wm = w - 1;
        int hm = h - 1;
        int wh = w * h;
        int div = radius + radius + 1;

        int r[] = new int[wh];
        int g[] = new int[wh];
        int b[] = new int[wh];
        int rSum, gSum, bSum, x, y, i, p, yp, yi, yw;
        int vMin[] = new int[Math.max(w, h)];

        int divSum = (div + 1) >> 1;
        divSum *= divSum;
        int dv[] = new int[256 * divSum];
        for (i = 0; i < 256 * divSum; i++) {
            dv[i] = (i / divSum);
        }

        yw = yi = 0;

        int[][] stack = new int[div][3];
        int stackPointer;
        int stackStart;
        int[] sir;
        int rbs;
        int r1 = radius + 1;
        int routSum, goutSum, boutSum;
        int rinSum, ginSum, binSum;

        for (y = 0; y < h; y++) {
            rinSum = ginSum = binSum = routSum = goutSum = boutSum = rSum = gSum = bSum = 0;
            for (i = -radius; i <= radius; i++) {
                p = pix[yi + Math.min(wm, Math.max(i, 0))];
                sir = stack[i + radius];
                sir[0] = (p & 0xff0000) >> 16;
                sir[1] = (p & 0x00ff00) >> 8;
                sir[2] = (p & 0x0000ff);
                rbs = r1 - Math.abs(i);
                rSum += sir[0] * rbs;
                gSum += sir[1] * rbs;
                bSum += sir[2] * rbs;
                if (i > 0) {
                    rinSum += sir[0];
                    ginSum += sir[1];
                    binSum += sir[2];
                } else {
                    routSum += sir[0];
                    goutSum += sir[1];
                    boutSum += sir[2];
                }
            }
            stackPointer = radius;

            for (x = 0; x < w; x++) {

                r[yi] = dv[rSum];
                g[yi] = dv[gSum];
                b[yi] = dv[bSum];

                rSum -= routSum;
                gSum -= goutSum;
                bSum -= boutSum;

                stackStart = stackPointer - radius + div;
                sir = stack[stackStart % div];

                routSum -= sir[0];
                goutSum -= sir[1];
                boutSum -= sir[2];

                if (y == 0) {
                    vMin[x] = Math.min(x + radius + 1, wm);
                }
                p = pix[yw + vMin[x]];

                sir[0] = (p & 0xff0000) >> 16;
                sir[1] = (p & 0x00ff00) >> 8;
                sir[2] = (p & 0x0000ff);

                rinSum += sir[0];
                ginSum += sir[1];
                binSum += sir[2];

                rSum += rinSum;
                gSum += ginSum;
                bSum += binSum;

                stackPointer = (stackPointer + 1) % div;
                sir = stack[(stackPointer) % div];

                routSum += sir[0];
                goutSum += sir[1];
                boutSum += sir[2];

                rinSum -= sir[0];
                ginSum -= sir[1];
                binSum -= sir[2];

                yi++;
            }
            yw += w;
        }
        for (x = 0; x < w; x++) {
            rinSum = ginSum = binSum = routSum = goutSum = boutSum = rSum = gSum = bSum = 0;
            yp = -radius * w;
            for (i = -radius; i <= radius; i++) {
                yi = Math.max(0, yp) + x;

                sir = stack[i + radius];

                sir[0] = r[yi];
                sir[1] = g[yi];
                sir[2] = b[yi];

                rbs = r1 - Math.abs(i);

                rSum += r[yi] * rbs;
                gSum += g[yi] * rbs;
                bSum += b[yi] * rbs;

                if (i > 0) {
                    rinSum += sir[0];
                    ginSum += sir[1];
                    binSum += sir[2];
                } else {
                    routSum += sir[0];
                    goutSum += sir[1];
                    boutSum += sir[2];
                }

                if (i < hm) {
                    yp += w;
                }
            }
            yi = x;
            stackPointer = radius;
            for (y = 0; y < h; y++) {
                pix[yi] = (0xff000000 & pix[yi]) | (dv[rSum] << 16) | (dv[gSum] << 8) | dv[bSum];

                rSum -= routSum;
                gSum -= goutSum;
                bSum -= boutSum;

                stackStart = stackPointer - radius + div;
                sir = stack[stackStart % div];

                routSum -= sir[0];
                goutSum -= sir[1];
                boutSum -= sir[2];

                if (x == 0) {
                    vMin[y] = Math.min(y + r1, hm) * w;
                }
                p = x + vMin[y];

                sir[0] = r[p];
                sir[1] = g[p];
                sir[2] = b[p];

                rinSum += sir[0];
                ginSum += sir[1];
                binSum += sir[2];

                rSum += rinSum;
                gSum += ginSum;
                bSum += binSum;

                stackPointer = (stackPointer + 1) % div;
                sir = stack[stackPointer];

                routSum += sir[0];
                goutSum += sir[1];
                boutSum += sir[2];

                rinSum -= sir[0];
                ginSum -= sir[1];
                binSum -= sir[2];

                yi += w;
            }
        }
        bitmap.setPixels(pix, 0, w, 0, 0, w, h);
        return bitmap;
    }

    /**
     * @param bitmap
     * @param radius
     * @return
     * @see #blurInJava(Bitmap, int)
     */
    private Bitmap blurInRenderScript(Bitmap bitmap, int radius) {
        if (radius > 0) {
            RenderScript rs = RenderScript.create(mContext);
            Allocation input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
            Allocation output = Allocation.createTyped(rs, input.getType());
            ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
            script.setRadius(Math.min(radius, 25));
            script.setInput(input);
            script.forEach(output);
            output.copyTo(bitmap);
        }
        return bitmap;
    }

    private class ShotTask extends AsyncTask<Integer, Void, Bitmap> {

        @Override
        protected final void onPreExecute() {
            hasStarted = true;
            if (mCallback != null) {
                mCallback.onStart();
            }
        }

        @Override
        protected final Bitmap doInBackground(Integer... params) {
            Bitmap bitmap = null;
            if (mCallback != null) {
                mSourceBitmap = mCallback.beforeAsync(mSourceBitmap);
            }
            bitmap = generate();
            if (mCallback != null) {
                bitmap = mCallback.afterAsync(bitmap);
            }
            return bitmap;
        }

        @Override
        protected final void onPostExecute(Bitmap result) {
            if (mCallback != null) {
                mCallback.onResult(result);
            }
            hasStarted = false;
        }
    }

    public static abstract class Callback {

        /**
         * Delay
         *
         * @param time
         */
        protected final void delay(long time) {
            try {
                Thread.sleep(time);
            } catch (Exception e) {
            }
        }

        /**
         * Perform in the thread.
         *
         * @see #afterAsync(Bitmap)
         */
        public Bitmap beforeAsync(Bitmap bitmap) {
            return bitmap;
        }

        /**
         * Perform in the thread.
         *
         * @param bitmap
         * @return
         * @see #beforeAsync(Bitmap)
         */
        public Bitmap afterAsync(Bitmap bitmap) {
            return bitmap;
        }

        /**
         * Run in the UI thread.
         *
         * @see #onResult(Bitmap)
         */
        public void onStart() {
        }

        /**
         * Run in the UI thread.
         *
         * @param result
         * @see #onStart()
         */
        public abstract void onResult(Bitmap result);
    }
}
